Android 中的智能指针是通过引用计数的方式方式来实现内存自动回收的。在大多数情况下我们使用强指针 sp 就好了,那么弱指针 wp 的存在意义有是什么呢?
从使用的角度来说,wp 扮演的是一个指针缓存的角色,想用时候可以用,同时尽可能少的避免副作用。其实,简单的裸指针也能很好地完成指针缓存的功能,其功能性并不是 wp 存在的必要条件。
wp 存在的核心原因是:解决循环引用导致的死锁问题。
这里的死锁是指广义的死锁(不局限于多线程编程): 两个对象相互限制,导致某些资源不能被第三者使用
# 1. 循环引用导致的死锁问题
接下来,我们就通过一个简单的示例程序来演示循环引用导致的死锁问题:
首先有两个类,其内部都有一个智能指针指向对方,形成循环引用:
class A : public RefBase
{
public:
A()
{
}
virtual ~A()
{
}
void setB(sp<B>& b)
{
mB = b;
}
private:
sp<B> mB;
}
class B : public RefBase
{
public:
B()
{
}
virtual ~B()
{
}
void setA(sp<A>& a)
{
mA = a;
}
private:
sp<A> mA;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
整体结构如下图所示:
接下来看 main 函数:
int main(int argc, char** argv)
{
//初始化两个指针
A *a = new A();
B *b = new B();
// 触发构造函数调用, spA 内部强弱计数值 (1,1)
sp<A> spA(a);
// 触发构造函数调用, spB 内部强弱计数值 (1,1)
sp<B> spB(b);
//setB 内部有赋值操作 mB = b,触发等于操作符函数重载
//spB 内部强弱计数值 (2,2)
spA->setB(spB);
//setA 内部有赋值操作 mA = a,触发等于操作符函数重载
//spA 内部强弱计数值 (2,2)
spB->setA(spA);
return 0;
// spA 析构 内部强弱计数值 (1,1),内存无法回收
// spB 析构 内部强弱计数值 (1,1),内存无法回收
}
//等于操作符函数重载
template<typename T>
sp<T>& sp<T>::operator =(const sp<T>& other) {
// Force m_ptr to be read twice, to heuristically check for data races.
T* oldPtr(*const_cast<T* volatile*>(&m_ptr));
T* otherPtr(other.m_ptr);
// 强弱引用计数分别加 1
if (otherPtr) otherPtr->incStrong(this);
if (oldPtr) oldPtr->decStrong(this);
if (oldPtr != *const_cast<T* volatile*>(&m_ptr)) sp_report_race();
m_ptr = otherPtr;
return *this;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
从这个示例可以看出,在循环引用的情况下,指针指针在作用域结束后,强弱引用计数值无法变回 (0,0),内存无法回收,导致内存泄漏;
# 2. 解决方案
只需要把其中一个智能指针改为弱引用即可解决上面的问题:
class A : public RefBase
{
public:
A()
{
}
virtual ~A()
{
}
void setB(sp<B>& b)
{
mB = b;
}
private:
sp<B> mB;
}
class B : public RefBase
{
public:
B()
{
}
virtual ~B()
{
}
//函数参数也要变一下
void setA(sp<A>& a)
{
//触发另外的等于操作符函数重载
mA = a;
}
private:
//这里改成 wp 弱引用
wp<A> mA;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
主函数稍作修改:
int main(int argc, char** argv)
{
//初始化两个指针
A *a = new A();
B *b = new B();
// 触发构造函数调用, spA 内部强弱计数值 (1,1)
sp<A> spA(a);
// 触发构造函数调用, spB 内部强弱计数值 (1,1)
sp<B> spB(b);
//setB 内部有赋值操作 mB = b,触发等于操作符函数重载
//spB 内部强弱计数值 (2,2)
spA->setB(spB);
//setA 内部有赋值操作 mA = a,触发等于操作符函数重载
//spA 内部强弱计数值 (1,2)
spB->setA(spA);
return 0;
// spB 析构 内部强弱计数值 (1,1),内存无法回收
// spA 析构 内部强弱计数值 (0,1),强引用为 0 ,回收 sp<A> spA 内部的目标对象 A,
// 随着 A 的析构, A 的成员变量 mB 也开始析构, 目标对象 B 强弱引用计数减 1,内部强弱计数值变为 (0,0),回收目标对象 B 以及内部管理对象,B 对象的内存回收工作完成,接着触发 B 对象的成员 mA 的析构函数
// mA 执行析构函数,弱引用计数减 1,内部强弱计数值变为 (0,0),回收 A 对象内部对应的管理对象,A 对象的内存回收工作完成
}
//等于操作符函数重载
template<typename T>
wp<T>& wp<T>::operator = (const sp<T>& other)
{
weakref_type* newRefs =
other != nullptr ? other->createWeak(this) : nullptr; //增加弱引用计数
T* otherPtr(other.m_ptr);
if (m_ptr) m_refs->decWeak(this);
m_ptr = otherPtr;
m_refs = newRefs;
return *this;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
当程序的一个引用修改为 wp 时,main 函数结束时:
- spB 析构 内部强弱计数值 (1,1),内存无法回收
- spA 析构 内部强弱计数值 (0,1),强引用为 0 ,回收 sp<A> spA 内部的目标对象 A,
- 随着 A 的析构, A 的成员变量 mB 也开始析构, 目标对象 B 强弱引用计数减 1,内部强弱计数值变为 (0,0),回收目标对象 B 以及内部管理对象,B 对象的内存回收工作完成,接着触发 B 对象的成员 mA 的析构函数
- mA 执行析构函数,弱引用计数减 1,内部强弱计数值变为 (0,0),回收 A 对象内部对应的管理对象,A 对象的内存回收工作完成
这样就解决了上一节中提出的内存泄漏问题!
# 3. 总结
- wp 的基本作用:wp 扮演了指针缓存的角色,想用时候可以用,但不想因此阻止资源被释放
- wp 存在的根本原因:解决循环引用导致的死锁问题